Spring Security 프로젝트 설정 2 - JwtService와 Filter 설정
✒️ 2025-05-28 14:21 내용 수정
-
가이드 영상 : Amigoscode's Spring Boot 3 + Spring Security 6 - JWT Authentication and Authorisation
-
Spring Security의 가이드 영상을 찾아보던 중 위 영상이 도움이 되어 영상 내용을 직접 따라하며 내용을 정리하였다.
- 이전에 Spring Security 5.x 버전 영상을 참고했다가 Spring Security Config에서 많은 부분이 달라져서 Spring Security 6.x 버전 영상을 다시 찾아 정리했다.
-
SpringSecurity 프로젝트 설정 목록
- Spring Security 기본 사용자 추가 및 테스트
- Spring Security 프로젝트 설정 1 - DB연결과 JPA 설정
- Spring Security 프로젝트 설정 2 - JwtService와 Filter 설정
- Spring Security 프로젝트 설정 3 - Security Config
- Spring Security 프로젝트 설정 4 - Authentication Service와 Controller
- Spring Security 프로젝트 설정 5 - Security CORS 설정
- Spring Security 프로젝트 설정 6 - JWT Refresh Token 생성 및 저장
- Spring Security 프로젝트 설정 7 - JWT Refresh Token 재발급
- Spring Security 프로젝트 설정 8 - JWT 클라이언트 저장
- Spring Security 프로젝트 설정 9 - JWT 로그아웃
- Spring Security 프로젝트 설정 10 - 권한 설정
흐름

Service 추가
config패키지를 생성하고,JwtService를 추가한다.- JWT에서 사용자 이름을 추출하거나 토큰의 유효성 검사, 토큰 서명 등의 기능을 수행하는 메소드를 저장한 Service다.
- 이 부분에서 JWT를 다루는 메소드가 필요하기에 JWT 의존성이 없으면 진행할 수 없다.
io.jsonwebtoken패키지 메소드들이다.
SECRET_KEY는 암호화된 265 bit Hex 키를 암호화 키 생성 사이트에서 생성했다.- 이 Key는 JWT 서명을 생성하고 검증하는데 필요하기 때문에 프로젝트에서 사용 시 외부로 노출되지 않는 파일에 저장해야 한다.
- 여기선 테스트용으로만 사용하여 클래스 내에 저장했다.
package com.example.security.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Service
public class JwtService {
private static final String SECRET_KEY =
"d7e3c737696c3b9241bbde3fdfb664b515f36bb634da1afac2e45a85faef6c37";
// 토큰에서 사용자 이름 추출
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 클레임 추출
public <T> T extractClaim(
String token,
Function<Claims, T> claimsResolver
) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// 토큰 생성 - UserDetail로만 생성
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
// 토큰 생성
public String generateToken(
Map<String, Object> extraClaims, // 토큰에 보낼 정보
UserDetails userDetails
) {
return Jwts
.builder()
.setClaims(extraClaims) // 클레임 추가
.setSubject(userDetails.getUsername()) // subject 추가
.setIssuedAt(
new Date(
System.currentTimeMillis())) // 토큰 발행일
.setExpiration(
new Date(
System.currentTimeMillis()
+ 1000 * 60 * 60 * 24)) // 24시간
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
// 토큰 유효성 검사
public boolean isTokenValid(
String token,
UserDetails userDetails
) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()))
&& !isTokenExpired(token);
}
// 토큰 만료 확인
public boolean isTokenExpired(String token) {
return extraExpiration(token).before(new Date());
}
// 토큰에서 만료 기한 가져오기
public Date extraExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// jwt에서 모든 클레임 추출
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// jwt 서명에 사용하는 비밀 키 생성
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes);
}
}
JwtService에서 사용할UserDetailsService를 생성하기 위한ApplicationConfig클래스를 추가한다.
package com.example.security.config;
import com.example.security.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {
private final UserRepository userRepository;
@Bean
public UserDetailsService userDetailsService() {
return username -> userRepository.findByEmail(username)
.orElseThrow(()->new UsernameNotFoundException("User not found"));
}
}
JwtAuthenticationFilter 추가
config패키지에JwtAuthenticationFilter클래스를 추가하고,OncePerRequestFilter를 상속 받는다.OncePerRequestFilter: 매 HTTP 요청마다 한 번만 실행하는Filter로, 요청 전후에 공통 작업을 수행할 때 사용한다.- 로그 기록, 보안 검증, session 관리 등
- 사용자가 보호된 라우트나 자원에 접근하고자 할 때 사용자 에이전트는 Authorization header에 Bearer schema를 사용하여 JWT를 전송해야 하므로, 토큰 검증 시
Bearer를 사용하여 확인할 수 있다.
package com.example.security.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
// 요청이 들어왔을 때 처리할 작업
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request, // 요청
@NonNull HttpServletResponse response, // 응답
@NonNull FilterChain filterChain // 필터들
) throws ServletException, IOException {
// 요청으로부터 온 header의 내용 추출
final String authHeader = request.getHeader("Authorization");
// jwt
final String jwt;
// 사용자 이메일
final String userEmail;
// jwt가 없으면 요청을 이후 필터로 전달
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
// jwt는 Authorization header에 Bearer schema를 사용한다.
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7); // "Bearer "는 7글자
// jwt로부터 사용자 이메일을 추출
userEmail = jwtService.extractUsername(jwt);
// 검증 절차
// 사용자가 존재하고, 아직 인증을 진행하지 않아
// SecurityContextHolder에 저장되지 않았을 때
if (userEmail != null
&& SecurityContextHolder
.getContext()
.getAuthentication() == null) {
UserDetails userDetails
= this
.userDetailsService
.loadUserByUsername(userEmail);
// jwt 유효성 확인
if (jwtService.isTokenValid(jwt, userDetails)) {
// Spring SecurityContext에 업데이트에 필요한 객체
UsernamePasswordAuthenticationToken authToken
= new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource()
.buildDetails(request)
);
SecurityContextHolder
.getContext()
.setAuthentication(authToken);
}
}
// 항상 작업이 끝나면 다음 필터로 넘겨줘야 함
filterChain.doFilter(request, response);
}
}